<!DOCTYPE html>

<head>
<title>tinychat</title>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="favicon.svg" rel="icon" type="image/svg+xml"/>
<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpine-collective/toolkit@1.0.2/dist/cdn.min.js"></script>
<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
<script defer="" src="/static/cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
<script defer="" src="/static/unpkg.com/@marcreichel/alpine-autosize@1.3.x/dist/alpine-autosize.min.js"></script>
<script defer="" src="/static/unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="/static/unpkg.com/dompurify@3.1.5/dist/purify.min.js"></script>
<script src="/static/unpkg.com/marked@13.0.0/marked.min.js"></script>
<script src="/static/unpkg.com/marked-highlight@2.1.2/lib/index.umd.js"></script>
<script src="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/highlight.min.js"></script>
<script src="/index.js"></script>
<link href="/static/fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="/static/fonts.gstatic.com" rel="preconnect"/>
<link href="/static/fonts.googleapis.com/css2" rel="stylesheet"/>
<link href="/static/cdn.jsdelivr.net/npm/purecss@3.0.0/build/base-min.css" rel="stylesheet"/>
<link crossorigin="anonymous" href="/static/cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" referrerpolicy="no-referrer" rel="stylesheet">
<link href="/static/unpkg.com/@highlightjs/cdn-assets@11.9.0/styles/vs2015.min.css" rel="stylesheet"/>
<link href="/index.css" rel="stylesheet"/>
<link href="/common.css" rel="stylesheet"/>
</head>
<body>
<main x-data="state" x-init="console.log(endpoint)">
     <!-- Error Toast -->
    <div x-show="errorMessage" x-transition.opacity class="toast">
        <div class="toast-header">
            <span class="toast-error-message" x-text="errorMessage.basic"></span>
            <div class="toast-header-buttons">
                <button @click="errorExpanded = !errorExpanded; if (errorTimeout) { clearTimeout(errorTimeout); errorTimeout = null; }" 
                        class="toast-expand-button" 
                        x-show="errorMessage.stack">
                    <span x-text="errorExpanded ? 'Hide Details' : 'Show Details'"></span>
                </button>
                <button @click="errorMessage = null; errorExpanded = false;" class="toast-close-button">
                    <i class="fas fa-times"></i>
                </button>
            </div>
        </div>
        <div class="toast-content" x-show="errorExpanded" x-transition>
            <span x-text="errorMessage.stack"></span>
        </div>
    </div>
<div class="model-selector">
  <select @change="if (cstate) cstate.selectedModel = $event.target.value" x-model="cstate.selectedModel" x-init="await populateSelector()" class='model-select'>
  </select>
</div>
<div @popstate.window="
      if (home === 2) {
        home = -1;
        cstate = { time: null, messages: [], selectedModel: 'llama-3.1-8b' };
        time_till_first = 0;
        tokens_per_second = 0;
        total_tokens = 0;
      }
    " class="home centered" x-effect="
      $refs.inputForm.focus();
      if (home === 1) setTimeout(() =&gt; home = 2, 100);
      if (home === -1) setTimeout(() =&gt; home = 0, 100);
    " x-show="home === 0" x-transition="">
<h1 class="title megrim-regular">tinychat</h1>
<template x-if="histories.length">
  <button 
    @click="if(confirm('Are you sure you want to clear all history?')) clearAllHistory();" 
    class="clear-history-button">
    <i class="fas fa-trash"></i> Clear All History
  </button>
</template>
<div class="histories-container-container">
<template x-if="histories.length">
<div class="histories-start"></div>
</template>
<div class="histories-container" x-intersect="
          $el.scrollTo({ top: 0, behavior: 'smooth' });
        ">
<template x-for="_state in histories.toSorted((a, b) =&gt; b.time - a.time)">
<div @click="
            cstate = _state;
            if (cstate) cstate.selectedModel = document.querySelector('.model-selector select').value
            // updateTotalTokens(cstate.messages);
            home = 1;
            // ensure that going back in history will go back to home
            window.history.pushState({}, '', '/');
          " @touchend="
            if (Math.abs($event.changedTouches[0].clientX - otx) &gt; trigger) removeHistory(_state);
            $el.style.setProperty('--tx', 0);
            $el.style.setProperty('--opacity', 1);
          " @touchmove="
            $el.style.setProperty('--tx', $event.changedTouches[0].clientX - otx);
            $el.style.setProperty('--opacity', 1 - (Math.abs($event.changedTouches[0].clientX - otx) / trigger));
          " @touchstart="
            otx = $event.changedTouches[0].clientX;
          " class="history" x-data="{ otx: 0, trigger: 75 }">
<h3 x-text="new Date(_state.time).toLocaleString()"></h3>
<p x-text="$truncate(_state.messages[0].content, 80)"></p>
<!-- delete button -->
<button @click.stop="removeHistory(_state);" class="history-delete-button">
<i class="fas fa-trash"></i>
</button>
</div>
</template>
</div>
<template x-if="histories.length">
<div class="histories-end"></div>
</template>
</div>
</div>
<div class="messages" x-init="
      $watch('cstate', value =&gt; {
        $el.innerHTML = '';
        value.messages.forEach(({ role, content }) =&gt; {
          const div = document.createElement('div');
          div.className = `message message-role-${role}`;
          try {
            div.innerHTML = DOMPurify.sanitize(marked.parse(content));
          } catch (e) {
            console.log(content);
            console.error(e);
          }

          // add a clipboard button to all code blocks
          const codeBlocks = div.querySelectorAll('.hljs');
          codeBlocks.forEach(codeBlock =&gt; {
            const button = document.createElement('button');
            button.className = 'clipboard-button';
            button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;';
            button.onclick = () =&gt; {
              // navigator.clipboard.writeText(codeBlock.textContent);
              const range = document.createRange();
              range.setStartBefore(codeBlock);
              range.setEndAfter(codeBlock);
              window.getSelection()?.removeAllRanges();
              window.getSelection()?.addRange(range);
              document.execCommand('copy');
              window.getSelection()?.removeAllRanges();

              button.innerHTML = '&lt;i class=\'fas fa-check\'&gt;&lt;/i&gt;';
              setTimeout(() =&gt; button.innerHTML = '&lt;i class=\'fas fa-clipboard\'&gt;&lt;/i&gt;', 1000);
            };
            codeBlock.appendChild(button);
          });

          $el.appendChild(div);
        });

        $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
      });
    " x-intersect="
      $el.scrollTo({ top: $el.scrollHeight, behavior: 'smooth' });
    " x-ref="messages" x-show="home === 2" x-transition="">
</div>

<!-- Download Progress Section -->
<template x-if="downloadProgress && downloadProgress.length > 0">
  <div class="download-progress message message-role-assistant">
    <h2>Download Progress</h2>
    <br>
    <template x-for="(progress, index) in downloadProgress" :key="index">
      <div class="download-progress-node">
        <br>
        <h3 x-text="`Download ${index + 1}`"></h3>
        <p><strong>Model:</strong> <span x-text="progress.repo_id + '@' + progress.repo_revision"></span></p>
        <p><strong>Status:</strong> <span x-text="progress.status"></span></p>
        <div class="progress-bar-container">
          <div class="progress-bar" 
               :class="progress.isComplete ? 'complete' : 'in-progress'"
               :style="`width: ${progress.percentage}%;`">
          </div>
        </div>
        <template x-if="!progress.isComplete">
          <div>
            <p><strong>Progress:</strong> <span x-text="`${progress.downloaded_bytes_display} / ${progress.total_bytes_display} (${progress.percentage}%)`"></span></p>
            <p><strong>Speed:</strong> <span x-text="progress.overall_speed_display || 'N/A'"></span></p>
            <p><strong>ETA:</strong> <span x-text="progress.overall_eta_display || 'N/A'"></span></p>
          </div>
        </template>
      </div>
    </template>
  </div>
</template>


<div class="input-container">
<div class="input-performance">
<span class="input-performance-point">
<p class="monospace" x-text="(time_till_first / 1000).toFixed(2)"></p>
<p class="megrim-regular">SEC TO FIRST TOKEN</p>
</span>
<span class="input-performance-point">
<p class="monospace" x-text="tokens_per_second.toFixed(1)"></p>
<p class="megrim-regular">TOKENS/SEC</p>
</span>
<span class="input-performance-point">
<p class="monospace" x-text="total_tokens"></p>
<p class="megrim-regular">TOKENS</p>
</span>
</div>
<div class="input">
<button @click="$refs.imageUpload.click()" class="image-input-button" x-show="cstate.selectedModel === 'llava-1.5-7b-hf'">
<i class="fas fa-image"></i>
</button>
<input @change="$data.handleImageUpload($event)" accept="image/*" id="image-upload" style="display: none;" type="file" x-ref="imageUpload"/>
<div class="image-preview-container" x-show="imagePreview">
<img :src="imagePreview" alt="Uploaded Image" class="image-preview"/>
<button @click="imagePreview = null; imageUrl = null;" class="remove-image-button">
<i class="fas fa-times"></i>
</button>
</div>
<textarea :disabled="generating" :placeholder="generating ? 'Generating...' : 'Say something'" @input="
            home = (home === 0) ? 1 : home
            if (cstate.messages.length === 0 &amp;&amp; $el.value === '') home = -1;

            if ($el.value !== '') {
              const messages = [...cstate.messages];
              messages.push({ role: 'user', content: $el.value });
              // updateTotalTokens(messages);
            } else {
              if (cstate.messages.length === 0) total_tokens = 0;
              // else updateTotalTokens(cstate.messages);
            }
          " @keydown.enter="await handleEnter($event)" @keydown.escape.window="$focus.focus($el)" autofocus="" class="input-form" id="input-form" rows="1" x-autosize="" x-effect="
            console.log(generating);
            if (!generating) $nextTick(() =&gt; {
              $el.focus();
              setTimeout(() =&gt; $refs.messages.scrollTo({ top: $refs.messages.scrollHeight, behavior: 'smooth' }), 100);
            });
          " x-ref="inputForm"></textarea>
<button :disabled="generating" @click="await handleSend()" class="input-button">
<i :class="generating ? 'fa-spinner fa-spin' : 'fa-paper-plane'" class="fas"></i>
</button>
</div>
</div>
</main>
</body>
